﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using LayeredArchitectureSample.DomainModel;
using LayeredArchitectureSample.Infrastructure;

namespace LayeredArchitectureSample.DataAccess
{
    /// <summary>
    /// </summary>
    /// <remarks> Use TransactionScope to get transactions across several repository operations</remarks>
    public class EntityFrameworkTripRepository : ITripRepository
    {
        private readonly IDatabaseContextFactory _dataContextFactory;
        private readonly TripStateMapper _tripStateMapper;

        internal EntityFrameworkTripRepository(
            IDatabaseContextFactory dataContextFactory,
            TripStateMapper tripStateMapper)
        {
            if (tripStateMapper == null) throw new ArgumentNullException("tripStateMapper");

            _dataContextFactory = dataContextFactory;
            _tripStateMapper = tripStateMapper;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="id"></param>
        /// <exception cref="CannotRemoveEntityException"></exception>
        public void RemoveTrip(long id)
        {
            throw new NotSupportedException(/*TODO*/);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        /// <exception cref="CannotAddEntityException"></exception>
        /// <exception cref="ArgumentException"></exception>
        public DomainModel.Trip AddNewTrip(DomainModel.Route route, IEnumerable<WareUnitsCounted> wares)
        {
            var newTrip = new DomainModel.Trip(new TripEnRoute(1, wares, route));

            using (LayeredSampleDataModel dataContext = _dataContextFactory.CreateDatabaseContext())
            {
                var databaseTrip = new Trip()
                                       {
                                           Status = _tripStateMapper.GetTripStateForDatabase(newTrip),
                                           RouteId = route.Id
                                       };

                try
                {
                    foreach (DomainModel.WareUnitsCounted ware in wares)
                    {
                        var wareInTrip = new WareInTrip()
                        {
                            Quantity = ware.Quantity,
                            WareId = ware.Ware.Id,
                            Trip = databaseTrip
                        };

                        dataContext.WareInTrips.AddObject(wareInTrip);

                        databaseTrip.Wares.Add(wareInTrip);
                    }

                    dataContext.Trips.AddObject(databaseTrip);

                    dataContext.SaveChanges();

                    return new DomainModel.Trip(new DomainModel.Trip(new TripEnRoute(databaseTrip.Id, wares, route)));
                }
                catch (EntityException exception)
                {
                    throw new CannotAddEntityException(Resources.FailedToCreateTrip, exception);
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="EntityRetrievalException"></exception>
        /// <exception cref="EntityNotFoundException"></exception>
        public DomainModel.Trip GetTripById(long id)
        {
            try
            {
                using (LayeredSampleDataModel dataContext = _dataContextFactory.CreateDatabaseContext())
                {
                    var trip = GetTripFromContext(dataContext, id);

                    return new DomainModel.Trip(CreateTripStateFromDatabaseModel(dataContext, trip));
                }
            }
            catch (InvalidOperationException)
            {
                throw new EntityNotFoundException(Resources.FoundNoTripWithGivenId.FormatWith(id));
            }
            catch (EntityRetrievalException exception)
            {
                throw new EntityRetrievalException(Resources.FailedToRetrieveTrip, exception);
            }
            catch (EntityException exception)
            {
                throw new EntityRetrievalException(Resources.FailedToRetrieveTrip, exception);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="dataContext"></param>
        /// <param name="id"></param>
        /// <returns></returns>
        /// <exception cref="InvalidOperationException"></exception>
        private static Trip GetTripFromContext(LayeredSampleDataModel dataContext, long id)
        {
            // TODO: fish for an extension that gives us compile-time checks here
            return dataContext.Trips.Include("Route.RouteSteps.Location").Include("Wares.Ware").Where(t => t.Id == id).Single();
        }

        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="EntityNotFoundException"></exception>
        /// <exception cref="FailedToUpdateEntityException"></exception>
        public void PersistChanges(DomainModel.Trip trip)
        {
            if (trip == null) throw new ArgumentNullException("trip");

            try
            {
                using (LayeredSampleDataModel dataContext = _dataContextFactory.CreateDatabaseContext())
                {
                    Trip persistedTrip = GetPersistedTripForUpdate(trip, dataContext);

                    // TODO: test?
                    var modifiedTrip = new Trip();

                    modifiedTrip.Id = trip.Id;
                    modifiedTrip.RouteId = trip.Route.Id;
                    modifiedTrip.Status = _tripStateMapper.GetTripStateForDatabase(trip);

                    // TODO: optimize, generalize

                    var persistedTripWares = persistedTrip.Wares.ToDictionary(w => w.WareId, w=> w);

                    foreach (DomainModel.WareUnitsCounted ware in trip.Wares)
                    {
                        var wareInTrip = new WareInTrip()
                        {
                            Quantity = ware.Quantity,
                            WareId = ware.Ware.Id,
                            TripId = trip.Id
                        };

                        WareInTrip persistedWareInfo;

                        if (!persistedTripWares.TryGetValue(ware.Ware.Id, out persistedWareInfo))
                        {
                            dataContext.WareInTrips.AddObject(wareInTrip);

                            modifiedTrip.Wares.Add(wareInTrip);
                        }
                        else
                        {
                            wareInTrip.Id = persistedWareInfo.Id;

                            dataContext.WareInTrips.ApplyCurrentValues(wareInTrip);
                        }
                    }

                    WareInTrip[] persistedWareInfoAsArray = persistedTrip.Wares.ToArray();

                    foreach (WareInTrip wareInTrip in persistedWareInfoAsArray)
                    {
                        if (!trip.Wares.Any(w=>w.Ware.Id == wareInTrip.WareId))
                        {
                            dataContext.DeleteObject(wareInTrip);
                        }
                    }

                    dataContext.Trips.ApplyCurrentValues(modifiedTrip);

                    dataContext.SaveChanges();
                }
            }
            catch (EntityException exception)
            {
                throw new FailedToUpdateEntityException(Resources.FailedToUpdateTrip, exception);
            }
        }

        private static Trip GetPersistedTripForUpdate(DomainModel.Trip trip, LayeredSampleDataModel dataContext)
        {
            try
            {
                return GetTripFromContext(dataContext, trip.Id);
            }
            catch (InvalidOperationException)
            {
                throw new EntityNotFoundException(
                    Resources.FailedToUpdateTripNoTripWithGivenId.FormatWith(trip.Id));
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        /// <exception cref="EntityRetrievalException"></exception>
        public IEnumerable<DomainModel.Trip> GetAll()
        {
            try
            {
                using (LayeredSampleDataModel dataContext = _dataContextFactory.CreateDatabaseContext())
                {
                    var all = new List<DomainModel.Trip>();

                    foreach (Trip trip in dataContext.Trips)
                    {
                        ITrip tripState = CreateTripStateFromDatabaseModel(dataContext, trip);

                        all.Add(new DomainModel.Trip(tripState));
                    }

                    return all;
                }
            }
            catch (EntityRetrievalException exception)
            {
                throw new EntityRetrievalException(Resources.FailedToRetrieveTrips, exception);
            }
            catch (EntityException exception)
            {
                throw new EntityRetrievalException(Resources.FailedToRetrieveTrips, exception);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        /// <param name="trip"></param>
        /// <returns></returns>
        /// <exception cref="EntityException"></exception>
        /// <exception cref="EntityRetrievalException"></exception>
        private static ITrip CreateTripStateFromDatabaseModel(LayeredSampleDataModel context, Trip trip)
        {
            ITrip tripState;

            // TODO: can we make this prettier? Or auto-generate?

            if ((trip.Status == TripState.Arrived.ToString()) || (trip.Status == TripState.EnRoute.ToString()))
            {
                Location[] wayPoints = GetWaypointsForRoute(context, trip.RouteId.Value);

                IEnumerable<DomainModel.WareUnitsCounted> wares = trip.Wares.Select(w => new DomainModel.WareUnitsCounted(new DomainModel.Ware(w.Ware.Id, w.Ware.Code), w.Quantity));
                var route = new DomainModel.Route(trip.Route.Id, wayPoints.Select(w => new DomainModel.Location(w.Id, w.Name)));

                if (trip.Status == TripState.Arrived.ToString())
                {
                    tripState = new TripThatHasArrived(trip.Id, wares, route);
                }
                else
                {
                    tripState = new TripEnRoute(trip.Id, wares, route);
                }
            }
            else
            {
                throw new EntityRetrievalException("Cannot create a trip from status {0}".FormatWith(trip.Status));
            }
            
            return tripState;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="context"></param>
        /// <param name="routeId"></param>
        /// <returns></returns>
        /// <exception cref="EntityException"></exception>
        private static Location[] GetWaypointsForRoute(LayeredSampleDataModel context, long routeId)
        {
            return (from RouteStep step in context.RouteSteps
                    where step.RouteId == routeId
                    orderby step.StepNumber // TODO: check this out, this ain't enough
                    select step.Location).ToArray();
        }
    }
}